<?php

declare(strict_types=1);

namespace App\Controller\Webhook;

use App\Repository\WebhookRepository;
use App\Repository\WebhookRequestRepository;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Yiisoft\Http\Header;
use Yiisoft\Http\Status;
use Yiisoft\Router\CurrentRoute;

use function explode;
use function is_string;
use function ltrim;
use function preg_split;
use function str_contains;
use function trim;

final readonly class CaptureAction
{
    public function __construct(
        private WebhookRepository $webhooks,
        private WebhookRequestRepository $requests,
        private ResponseFactoryInterface $responseFactory,
        private StreamFactoryInterface $streamFactory,
        private CurrentRoute $currentRoute,
    ) {}

    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        $token = $this->currentRoute->getArgument('token');

        if ($token === null) {
            $response = $this->responseFactory->createResponse(Status::NOT_FOUND);
            $response->getBody()->write('Webhook not found.');
            return $response->withHeader(Header::CONTENT_TYPE, 'text/plain; charset=utf-8');
        }

        $webhook = $this->webhooks->findByToken($token);
        if ($webhook === null) {
            $response = $this->responseFactory->createResponse(Status::NOT_FOUND);
            $response->getBody()->write('Webhook not found.');
            return $response->withHeader(Header::CONTENT_TYPE, 'text/plain; charset=utf-8');
        }

        $suffix = $this->currentRoute->getArgument('suffix') ?? '';

        $uri = $request->getUri();
        $scheme = $uri->getScheme() ?: 'http';
        $host = $uri->getHost();
        $port = $uri->getPort();
        if ($port !== null && $port !== 80 && $port !== 443) {
            $host .= ':' . $port;
        }

        $rawBody = $this->readBody($request);

        $serverParams = $request->getServerParams();
        $ipAddress = null;
        if (isset($serverParams['HTTP_X_FORWARDED_FOR']) && is_string($serverParams['HTTP_X_FORWARDED_FOR'])) {
            $ipCandidates = preg_split('/\s*,\s*/', $serverParams['HTTP_X_FORWARDED_FOR']) ?: [];
            $ipAddress = $ipCandidates[0] ?? null;
        }
        if ($ipAddress === null && isset($serverParams['REMOTE_ADDR']) && is_string($serverParams['REMOTE_ADDR'])) {
            $ipAddress = $serverParams['REMOTE_ADDR'];
        }

        $headers = $request->getHeaders();
        $userAgent = null;
        if (isset($headers['User-Agent'][0])) {
            $userAgent = $headers['User-Agent'][0];
        } elseif (isset($headers['user-agent'][0])) {
            $userAgent = $headers['user-agent'][0];
        }

        $path = $suffix === '' ? '/' : '/' . ltrim($suffix, '/');

        $this->requests->record(
            $webhook['id'],
            $request->getMethod(),
            $scheme,
            $host,
            $path,
            $request->getQueryParams(),
            $headers,
            $rawBody,
            $ipAddress,
            $userAgent,
        );

        $this->webhooks->touch($webhook['id']);

        $response = $this->responseFactory->createResponse($webhook['response_status']);
        $response = $response->withHeader(Header::CONTENT_TYPE, $webhook['response_content_type']);

        foreach ($this->parseHeaders($webhook['response_headers']) as $headerName => $headerValue) {
            $response = $response->withHeader($headerName, $headerValue);
        }

        $stream = $this->streamFactory->createStream($webhook['response_body']);
        return $response->withBody($stream);
    }

    private function readBody(ServerRequestInterface $request): string
    {
        $body = $request->getBody();
        if ($body->isSeekable()) {
            $body->rewind();
        }

        return $body->getContents();
    }

    /**
     * @return array<string, string>
     */
    private function parseHeaders(string $rawHeaders): array
    {
        $headers = [];
        $lines = preg_split('/\r?\n/', $rawHeaders) ?: [];

        foreach ($lines as $line) {
            $line = trim($line);
            if ($line === '') {
                continue;
            }

            if (!str_contains($line, ':')) {
                continue;
            }

            [$name, $value] = explode(':', $line, 2);
            $name = trim($name);
            $value = trim($value);

            if ($name === '') {
                continue;
            }

            if (isset($headers[$name])) {
                $headers[$name] .= ', ' . $value;
            } else {
                $headers[$name] = $value;
            }
        }

        return $headers;
    }
}
